/*
 * Created on 29-Dec-2004
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */

package org.gudy.azureus2.pluginsimpl.local.clientid;

import java.io.InputStream;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.net.ssl.SSLSocketFactory;

import org.gudy.azureus2.core3.logging.LogAlert;
import org.gudy.azureus2.core3.logging.LogEvent;
import org.gudy.azureus2.core3.logging.LogIDs;
import org.gudy.azureus2.core3.logging.Logger;
import org.gudy.azureus2.core3.torrent.TOTorrent;
import org.gudy.azureus2.core3.util.AEThread2;
import org.gudy.azureus2.core3.util.BEncoder;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.ThreadPool;
import org.gudy.azureus2.core3.util.ThreadPoolTask;
import org.gudy.azureus2.plugins.clientid.ClientIDException;
import org.gudy.azureus2.plugins.clientid.ClientIDGenerator;
import org.gudy.azureus2.plugins.clientid.ClientIDManager;
import org.gudy.azureus2.pluginsimpl.local.torrent.TorrentImpl;

import com.aelitis.azureus.core.networkmanager.admin.NetworkAdmin;
import com.aelitis.azureus.core.util.NetUtils;

/**
 * @author parg
 * 
 */

public class ClientIDManagerImpl implements ClientIDManager {
    private static final LogIDs LOGID = LogIDs.PLUGIN;
    protected static ClientIDManagerImpl singleton = new ClientIDManagerImpl();

    protected static final char CR = '\015';
    protected static final char FF = '\012';
    protected static final String NL = "\015\012";

    public static ClientIDManagerImpl getSingleton() {
        return (singleton);
    }

    private ClientIDGenerator generator_user_accessor;
    private boolean use_filter;
    private boolean filter_override;
    private ThreadPool thread_pool;

    private int filter_port;

    public void setGenerator(ClientIDGenerator _generator, boolean _use_filter) {
        // I wanted to allow signed plugins the ability to do this but given that a malicious
        // plugin can use reflection to get access to fields (such as the URL field of a
        // URLClassLoader) I can't see a way to enforce this. That is, how can you verify
        // that the class was loaded from a signed jar? you can get the jar that the URLClassLoader
        // claims it was loaded from and verify that, but this jar location may have been changed
        // by the plugin. you can look inside the signed jar and check that there's a class in
        // there with the right name, implementing ClientIDGenerator, but this doesn't prove
        // that the implementation passed to this method is the same as once an offical signed
        // plugin is released that uses this feature (with, say, a class called a.b.c.X as the
        // generator), a malicious plugin can simply also implement a class a.b.c.X, ship
        // along with a copy of the official jar, hack the class-loader after loading to make
        // the class-loader point to the official jar. The only things that can't be changed
        // by reflection are static final fields which don't seem to help. We could modify
        // our security manager to trap a checkAccess perm check but we don't have access to
        // the thing being modified and this is used in various other places to work around bugs.
        // So we only accept generators loaded by non-plugin loaders. Note that you can't
        // change a class's class loader so this works.
        // we might be able to fix things by using some native storage that can't be modified
        // by a plugin, or by getting this code to load/instantiate the class, but you still
        // have the problem that the plugin can directly modify the "generator" field. Another
        // fix would be to enhance the security manager and provide methods to wrap the
        // setAccessible operations so we can control which objects are accessible

        checkGenerator(_generator);

        generator_user_accessor = _generator;
        use_filter = _use_filter;

        // we override the filter parameter here if we have a local bind IP set as
        // this is the only simple solution to enforcing the local bind (Sun's
        // HTTPConnection doesn't allow the network interface to be bound)

        if (!use_filter) {

            // another reason for NOT doing this is if the user has a defined proxy
            // in this case the assumption is that they know what they're doing and
            // the proxy will be bound correctly to ensure that things work...

            String http_proxy = System.getProperty("http.proxyHost");
            String socks_proxy = System.getProperty("socksProxyHost");

            NetworkAdmin network_admin = NetworkAdmin.getSingleton();

            if (network_admin.mustBind()) {

                filter_override = true;

                use_filter = true;

            } else {

                InetAddress bindIP = network_admin.getSingleHomedServiceBindAddress();

                if ((http_proxy == null || http_proxy.trim().length() == 0) && (socks_proxy == null || socks_proxy.trim().length() == 0)
                        && (bindIP != null && !bindIP.isAnyLocalAddress())) {

                    int ips = 0;

                    // seeing as this is a bit of a crappy way to enforce binding, add one more check to make
                    // sure that the machine has multiple ips before going ahead in case user has set it
                    // incorrectly

                    try {
                        List<NetworkInterface> x = NetUtils.getNetworkInterfaces();

                        for (NetworkInterface network_interface : x) {

                            Enumeration<InetAddress> addresses = network_interface.getInetAddresses();

                            while (addresses.hasMoreElements()) {

                                InetAddress address = addresses.nextElement();

                                if (!address.isLoopbackAddress()) {

                                    ips++;
                                }
                            }
                        }
                    } catch (Throwable e) {

                        Logger.log(new LogEvent(LOGID, "", e));
                    }

                    if (ips > 1) {

                        filter_override = true;

                        use_filter = true;

                        if (Logger.isEnabled())
                            Logger.log(new LogEvent(LOGID, "ClientIDManager: overriding filter " + "option to support local bind IP"));
                    }
                }
            }
        }

        if (use_filter) {

            try {
                thread_pool = new ThreadPool("ClientIDManager", 32);

                String connect_timeout = System.getProperty("sun.net.client.defaultConnectTimeout");
                String read_timeout = System.getProperty("sun.net.client.defaultReadTimeout");

                int timeout = Integer.parseInt(connect_timeout) + Integer.parseInt(read_timeout);

                thread_pool.setExecutionLimit(timeout);

                final ServerSocket ss = new ServerSocket(0, 1024, InetAddress.getByName("127.0.0.1"));

                filter_port = ss.getLocalPort();

                ss.setReuseAddress(true);

                new AEThread2("ClientIDManager::filterloop") {
                    public void run() {
                        long failed_accepts = 0;

                        while (true) {

                            try {
                                Socket socket = ss.accept();

                                failed_accepts = 0;

                                thread_pool.run(new httpFilter(socket));

                            } catch (Throwable e) {

                                failed_accepts++;

                                if (Logger.isEnabled())
                                    Logger.log(new LogEvent(LOGID, "ClientIDManager: listener failed on port " + filter_port, e));

                                if (failed_accepts > 10) {

                                    // looks like its not going to work...
                                    // some kind of socket problem

                                    Logger.logTextResource(new LogAlert(LogAlert.UNREPEATABLE, LogAlert.AT_ERROR, "Network.alert.acceptfail"),
                                            new String[] { "" + filter_port, "TCP" });

                                    use_filter = false;

                                    break;
                                }
                            }
                        }
                    }
                }.start();

                if (Logger.isEnabled())
                    Logger.log(new LogEvent(LOGID, "ClientIDManager: listener established on port " + filter_port));

            } catch (Throwable e) {

                Logger.logTextResource(new LogAlert(LogAlert.UNREPEATABLE, LogAlert.AT_ERROR, "Tracker.alert.listenfail"), new String[] { ""
                        + filter_port });

                if (Logger.isEnabled())
                    Logger.log(new LogEvent(LOGID, "ClientIDManager: listener failed on port " + filter_port, e));

                use_filter = false;
            }
        }
    }

    public ClientIDGenerator getGenerator() {
        checkGenerator(generator_user_accessor);

        return (generator_user_accessor);
    }

    protected void checkGenerator(ClientIDGenerator gen) {
        ClassLoader cl = gen.getClass().getClassLoader();

        if (cl != null && cl != ClientIDManager.class.getClassLoader()) {

            Debug.out("Generator isn't trusted - " + gen);

            throw (new RuntimeException("Generator isn't trusted"));
        }
    }

    public byte[] generatePeerID(TOTorrent torrent, boolean for_tracker)

    throws ClientIDException {
        return (getGenerator().generatePeerID(new TorrentImpl(torrent), for_tracker));
    }

    public void generateHTTPProperties(Properties properties)

    throws ClientIDException {
        if (use_filter) {

            // to support SSL here we would need to substitute the https url with an https one
            // and then drive the SSL in the filter appropriately

            URL url = (URL) properties.get(ClientIDGenerator.PR_URL);

            boolean is_ssl = url.getProtocol().toLowerCase().equals("https");

            /*
             * if ( !url.getProtocol().toLowerCase().equals( "http" )){ Logger.log(new LogAlert(LogAlert.UNREPEATABLE, LogAlert.AT_ERROR,
             * "ClientIDManager only supports filtering of http, not https")); return; }
             */

            try {
                String url_str = url.toString();

                String target_host = url.getHost();
                int target_port = url.getPort();

                if (target_port == -1) {

                    target_port = url.getDefaultPort();
                }

                int host_pos = url_str.indexOf(target_host);

                String new_url = url_str.substring(0, host_pos) + "127.0.0.1:" + filter_port;

                if (is_ssl) {

                    new_url = "http" + new_url.substring(new_url.indexOf(':'));
                }

                String rem = url_str.substring(host_pos + target_host.length());

                if (rem.charAt(0) == ':') {

                    rem = rem.substring(("" + target_port).length() + 1);
                }

                int q_pos = rem.indexOf('?');

                new_url +=
                        rem.substring(0, q_pos + 1) + "cid=" + (is_ssl ? "." : "") + target_host + ":" + target_port + "&"
                                + rem.substring(q_pos + 1);

                properties.put(ClientIDGenerator.PR_URL, new URL(new_url));

            } catch (Throwable e) {

                Debug.printStackTrace(e);
            }
        } else {

            getGenerator().generateHTTPProperties(properties);
        }
    }

    protected class httpFilter extends ThreadPoolTask {
        private Socket socket;

        protected httpFilter(Socket _socket) {
            socket = _socket;
        }

        public void runSupport() {
            String report_error = null;
            int written = 0;

            try {

                setTaskState("reading header");

                InputStream is = socket.getInputStream();

                byte[] buffer = new byte[1024];

                String header = "";

                while (true) {

                    int len = is.read(buffer);

                    if (len == -1) {

                        break;
                    }

                    header += new String(buffer, 0, len, Constants.BYTE_ENCODING);

                    if (header.endsWith(NL + NL) || header.indexOf(NL + NL) != -1) {

                        break;
                    }
                }

                List lines = new ArrayList();

                int pos = 0;

                while (true) {

                    int p1 = header.indexOf(NL, pos);

                    String line;

                    if (p1 == -1) {

                        line = header.substring(pos);

                    } else {

                        line = header.substring(pos, p1);
                    }

                    line = line.trim();

                    if (line.length() > 0) {

                        lines.add(line);
                    }

                    if (p1 == -1) {

                        break;
                    }

                    pos = p1 + 2;
                }

                String[] lines_in = new String[lines.size()];

                lines.toArray(lines_in);

                String get = lines_in[0];

                int p1 = get.indexOf("?cid=");
                int p2 = get.indexOf("&", p1);

                String cid = get.substring(p1 + 5, p2);

                int p3 = cid.lastIndexOf(":");

                String target_host = cid.substring(0, p3);
                int target_port = Integer.parseInt(cid.substring(p3 + 1));

                boolean is_ssl;

                if (target_host.startsWith(".")) {

                    is_ssl = true;

                    target_host = target_host.substring(1);

                } else {

                    is_ssl = false;
                }
                // fix up the Host: entry with the target details

                for (int i = 1; i < lines_in.length; i++) {

                    String line = lines_in[i];

                    if (line.toLowerCase().indexOf("host:") != -1) {

                        lines_in[i] = "Host: " + target_host + ":" + target_port;

                        break;
                    }
                }

                get = get.substring(0, p1 + 1) + get.substring(p2 + 1);

                lines_in[0] = get;

                String[] lines_out;

                if (filter_override) {

                    // bodge for ip override. we still need to take account of the correct
                    // user-agent
                    lines_out = lines_in;

                    Properties p = new Properties();

                    getGenerator().generateHTTPProperties(p);

                    String agent = p.getProperty(ClientIDGenerator.PR_USER_AGENT);

                    if (agent != null) {

                        for (int i = 0; i < lines_out.length; i++) {

                            if (lines_out[i].toLowerCase().startsWith("user-agent")) {

                                lines_out[i] = "User-Agent: " + agent;
                            }
                        }
                    }
                } else {

                    lines_out = getGenerator().filterHTTP(lines_in);
                }

                String header_out = "";

                for (int i = 0; i < lines_out.length; i++) {

                    header_out += lines_out[i] + NL;
                }

                header_out += NL;

                Socket target;

                if (is_ssl) {

                    target = SSLSocketFactory.getDefault().createSocket();

                } else {

                    target = new Socket();
                }

                try {
                    InetSocketAddress targetSockAddress = new InetSocketAddress(InetAddress.getByName(target_host), target_port);

                    InetAddress bindIP =
                            NetworkAdmin.getSingleton().getSingleHomedServiceBindAddress(
                                    targetSockAddress.getAddress() instanceof Inet6Address ? NetworkAdmin.IP_PROTOCOL_VERSION_REQUIRE_V6
                                            : NetworkAdmin.IP_PROTOCOL_VERSION_REQUIRE_V4);

                    if (bindIP != null) {

                        target.bind(new InetSocketAddress(bindIP, 0));
                    }

                    // System.out.println( "filtering " + target_host + ":" + target_port );

                    target.connect(targetSockAddress);

                    target.getOutputStream().write(header_out.getBytes(Constants.BYTE_ENCODING));

                    target.getOutputStream().flush();

                    InputStream target_is = target.getInputStream();

                    while (true) {

                        int len = target_is.read(buffer);

                        if (len == -1) {

                            break;
                        }

                        socket.getOutputStream().write(buffer, 0, len);

                        written += len;
                    }
                } finally {

                    target.close();
                }
            } catch (ClientIDException e) {

                report_error = e.getMessage();

            } catch (UnknownHostException e) {

                report_error = "Unknown host '" + e.getMessage() + "'";

            } catch (Throwable e) {

                // Debug.printStackTrace(e);

            } finally {

                if (report_error != null && written == 0) {

                    Map failure = new HashMap();

                    failure.put("failure reason", report_error);

                    try {
                        byte[] x = BEncoder.encode(failure);

                        socket.getOutputStream().write(x);

                    } catch (Throwable f) {

                        Debug.printStackTrace(f);
                    }
                }

                try {
                    socket.getOutputStream().flush();

                    socket.close();

                } catch (Throwable f) {

                }
            }
        }

        public void interruptTask() {
            try {
                /*
                 * if (Logger.isEnabled()) Logger.log(new LogEvent(LOGID, "ClientIDManager - interrupting " + "HTTP filter due to timeout"));
                 */
                socket.close();

            } catch (Throwable e) {

            }
        }
    }
}
